Add DiscreteBatchConstraint#765
Conversation
4a381ec to
1c3bbef
Compare
There was a problem hiding this comment.
Pull request overview
Adds a new batch-level discrete constraint (DiscreteBatchConstraint) that enforces a fixed discrete parameter value across all points in a recommended batch by optimizing over discrete (and hybrid) subspaces, aligning with the existing subspace-based infrastructure used for continuous cardinality constraints.
Changes:
- Introduces
DiscreteBatchConstraint(non-filtering, batch-level) plus validation preventing duplicate batch constraints per parameter. - Extends discrete/hybrid subspace infrastructure (
n_theoretical_subspaces, mask/config iterators, sampling helpers) and wires support intoBotorchRecommenderandRandomRecommenderwith compatibility gating for other recommenders. - Adds documentation and test coverage for discrete + hybrid subspace-generating constraints, replacing prior hybrid-only cardinality tests.
Reviewed changes
Copilot reviewed 14 out of 14 changed files in this pull request and generated 8 comments.
Show a summary per file
| File | Description |
|---|---|
baybe/constraints/discrete.py |
Adds DiscreteBatchConstraint and its subspace mask generation. |
baybe/constraints/validation.py |
Adds validation to prevent multiple batch constraints on the same parameter. |
baybe/constraints/__init__.py |
Exposes DiscreteBatchConstraint publicly. |
baybe/searchspace/discrete.py |
Adds discrete subspace-generating constraint plumbing and mask enumeration/sampling helpers. |
baybe/searchspace/continuous.py |
Renames/aligns to n_theoretical_subspaces and adds shuffled / with-replacement subspace configuration iteration. |
baybe/searchspace/core.py |
Adds combined discrete+continuous theoretical subspace counting and combined sampling utilities for hybrid spaces. |
baybe/recommenders/pure/base.py |
Adds supports_discrete_subspace_constraints gate and raises IncompatibilityError when unsupported. |
baybe/recommenders/pure/bayesian/botorch.py |
Implements discrete + hybrid optimization over subspaces when batch constraints are present. |
baybe/recommenders/pure/nonpredictive/sampling.py |
Updates RandomRecommender to respect discrete subspace-generating constraints. |
docs/userguide/constraints.md |
Documents DiscreteBatchConstraint and recommender compatibility. |
tests/constraints/test_batch_constraint.py |
Adds focused tests for the new constraint (behavior, validation, incompatibility, subspace counting/masks). |
tests/constraints/test_subspace_constraints_hybrid.py |
Adds parametrized hybrid tests covering discrete batch + (discrete/continuous) cardinality constraints. |
tests/constraints/test_cardinality_constraint_hybrid.py |
Removes older hybrid cardinality-only test file (superseded). |
CHANGELOG.md |
Records new user-facing feature entry. |
💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.
This comment was marked as resolved.
This comment was marked as resolved.
dd5bbf6 to
23b3348
Compare
AVHopp
left a comment
There was a problem hiding this comment.
I do not have any major comments. I personally find it a bit weird that the concept of a "partition" is somehow identical in both discrete and continuous subspaces, but caused by completely different things. However, I do not see any more elegant solution to this, and this might also be personal preference.
843793f to
a188d66
Compare
|
@AdrianSosic appreciate your review |
AdrianSosic
left a comment
There was a problem hiding this comment.
Hey @Scienfitz, thanks for the new feature, very good to have indeed. I'm already submitting the review so that you have something to work with, but I still need to read some parts of the partitioning logic
| @property | ||
| def n_inactive_parameter_combinations(self) -> int: | ||
| """The number of possible inactive parameter combinations.""" | ||
| def n_theoretical_partitions(self) -> int: |
There was a problem hiding this comment.
Let's keep the theoretical debate open until we've agreed on the resampling behavior for the discrete space
There was a problem hiding this comment.
please start the debate
There was a problem hiding this comment.
Ok, so now that we have arrived at the subset concept and have a clearer picture: would you still consider the theoretical necessary? Isn't it just fine to say we have n subsets that are spanned by the constraints? Or maybe give a concrete example (ideally with concrete numbers spanning the space) where you think the terminology is inappropriate.
There was a problem hiding this comment.
for me that was always sort of unconnected to the naming debate, but here how this property came to life:
the cardinality implementation already had such a property as its easy to calcualte the number of possuble subsets ad hoc + there are no other considerations for conti space.
When implementing for discrete spaces and batch constraint you realize that the number of subspaces is not sufficient to make a judgement (eg in dispatching logic). You actually need the number of subsets with at least M candidates, lets call it n_feasible_subsets
At the same time I did not want to implement n_feasible_subsets because it requires evaluating the size of each subset which would destroy generating them lazily.
So then I chose to make the name of the n_subsets more precise and give it a name that indicates its just an idealized number. I picked theoretical but am open to suggestions
On the other hand, and maybe thats what you're saying as well, we could just ignore this and say n_subsets is already sufficiently describing the property and doesn't need further precision naming at the small danger of a reader then not being aware of the mentioned subleties.
There was a problem hiding this comment.
I can follow the logic and would like to share two thoughts:
- I think you last paragraph summarizes it: in the end, it's just convention, right? We could say
subsetsare really just the sets that we are optimizing over, full stop. We've never explicitly said that there aren't potentially other sets you could build, which are however irrelevant to optimization problem. If we take this interpretation --> all good, right? - In my opinion, the actual core of the debate is still this thread that you closed. To me, the collection of subsets that you discard (and that makes the difference between
actualandtheoretical) should not be discarded in the first place – at least not without explicitly opting out by setting some flag likeallow_recommending_duplicates_within_batch. Because as long as that (non-existing) flag is set toTrue, you can always create an M-sized batch even if there is only 1 actual candidate left in the feasible region. So assuming that flag existed (and which we should probably add at some point), the whole debate sort of become obsolete in my opinion
There was a problem hiding this comment.
this PR does not implement or aim to implement the conditional duplicated sampling from batches which arises in the general recommendation context and not just for subspace-generating constraints, hence I closed that thread as I would much prefer a clean implementation of that potentially delicate topic
There is another obvious visual argument that should make you stop requesting to include more loosely-related changes into this PR:
![]()
Given that, this PR operates in the world where conditional duplicated sampling from batches is not present and aims to set up properties such that they work in the world this PR exists in
So in that world: is there still anything to discuss and should we rename the property? I already shared the minor disadvantage I see
There was a problem hiding this comment.
Don't get me wrong, I don't want you do implement more stuff in here. I just mentioned it to underpin my argument: if 1) the duplication stuff can be clearly resolved later and 2) there is a valid interpretation of the term subset that works for us --> I think it's fine if we drop the hypothetical part here and then take care of the proper handling of duplicates in a follow-up?
There was a problem hiding this comment.
- do you mean
theoretical? - if you vote to drop it we need a tie-break
There was a problem hiding this comment.
sorry, yes, I mean the theoretical. Summon your tie breakers :D
There was a problem hiding this comment.
@AVHopp @kalama-ai need a tie breaking here between the choices
- rename
n_theoretical_subsetsto justn_subsets - keep
n_theoretical_subsets
explanation why theoretical was added in the first place here
| def inactive_parameter_combinations( # noqa: DOC404 | ||
| self, | ||
| *, | ||
| shuffle: bool = False, |
There was a problem hiding this comment.
I have a suggestion for the shuffle argument here and in the corresponding discrete case. What I dislike about the method signature is that we have two seemingly independent arguments which are however coupled in reality (see your docstring for shuffle). This always opens the door for silent "errors", i.e. when someone provides both flags as True. We've had this countless times in other places and I urgently want to avoid these designs in the future (a la avoid invalid/meaningless configs by design).
For this situation here, we might have a very simple answer, which even simplifies the entire thing: as far as I can tell, there is no use case where we'd actually require a deterministic order, right? Also, I don't think there is a canonical default order – after all, this depends on the (arbitrary) order of constraints etc. So why not simply drop the argument altogether and just make shuffle True by default?
There was a problem hiding this comment.
as discussed: awaiting commit with suggestion
There was a problem hiding this comment.
So this is my suggestion: eab2277
Note that it does a few things, in fact:
- Drops the
shuffleargument and thereby resolves the problematic combination of arguments - Simplifies the internal logic accordingly
- Make the entire thing more readable by separating into three logical steps: 1) apply per constraint 2) construct iterator according to input flag 3) assemble output from iterator
One important thing to note: Technically, this logic is not 100% equivalent to the previous one, since this no longer results in a flat uniform sampling but rather aggregated over groups. However, there is currently no situation where we would need a truly uniform sample, and the proposed implementation comes with significantly reduced code. If you say that uniformity is important, I think it's still possible to meet halfway, but implementing only parts of the changes.
There was a problem hiding this comment.
hmm. wonder why you're glancing over this uniformity so fast, isn't it important?
- If its not uniform, its in some sense biased, even if we don't explore for now in detail how its baised. Being biased seems bad to me
sample_subset_masksfromdiscrete.pyusesisliceto select the first entries from the iterator. If its baised it will not guarantee a proper draw, I think it will even be a very badly structured draw, E.g. if group 1 shuffles to [B, A] and group 2 to [y, x], you always get (B,y), (B,x), (A,y), (A,x) — never e.g. (B,y), (A,x), (B,x), (A,y) - which doesnt make islice reasonable anymore. I remember that I specifically wanted to avoid this situation during development.
There is also a minor de-provement because infeasible samples are not removed from the pool anymore as before but appear now again
There was a problem hiding this comment.
Ok, how about now? Added two more commits:
- Restored the original logic but kept the
shufflechange and one logic simplification - Used existing numpy helpers for the flat index computation
There was a problem hiding this comment.
ok I am happy to keep the simplification in select_via_flat_index regardless (although I dont get why you removed the usually rated as helpful Examples/Notes)
But please take a look what your combined changes to the logic now achieved... there is almost no difference anymore. In particular nothing has become substantially shorter or simpler as claimed for your original reason to propose this change... so why do we not simply keep that tiny kwarg shuffle (even if only one of its possible values is used in the current code base)?
imo "improvements" like these are not even worth debating about...



also ngl I prefer the old version (left) here

as its more readable and uses the total variable which is now not removed
Co-authored-by: Alexander V. Hopp <alexander.hopp@merckgroup.com>
Consistent with the existing check-return-types=False setting.
Co-authored-by: AdrianSosic <adrian.sosic@merckgroup.com>
Co-authored-by: AdrianSosic <adrian.sosic@merckgroup.com>
Co-authored-by: AdrianSosic <adrian.sosic@merckgroup.com>
Co-authored-by: AdrianSosic <adrian.sosic@merckgroup.com>
Co-authored-by: AdrianSosic <adrian.sosic@merckgroup.com>
Co-authored-by: AdrianSosic <adrian.sosic@merckgroup.com>
- Rename DiscreteBatchConstraint.get_invalid to _get_invalid to align with the abstract method rename introduced on main - Add DiscreteBatchConstraint to DISCRETE_CONSTRAINTS_FILTERING_ORDER so build_constrained_product (introduced on main) can sort it - Ignore BadInitialCandidatesWarning in pytest.ini; the warning fires non-deterministically in heavily constrained spaces regardless of data volume
6224f62 to
615b581
Compare
2ff2f31 to
5b6e220
Compare
5b6e220 to
86aaaf9
Compare
Replace constraint-presence checks with n_theoretical_subsets > 0 for the with/without-subsets dispatch. This is more general: any future subset-generating constraint only needs to contribute to n_theoretical_subsets, without requiring changes to the dispatch logic.
Closes #583
Fixes #567
Functionality:
Adds a
DiscreteBatchConstraintwhich works via subspace generation just like the continuous cardinality constraint, just in the discrete searchspace part. While it will quickly lead to many partitions, it is also possible to use multipleDiscreteBatchConstraints and also combine them with the cardinality constraints because there is now a more unified interface for constraints that work via subspace generation.Notes:
subspaceis not optimal, because there is also the concept ofDiscreteSubspace/ContinuousSubspace. I changed the naming that references to creating subspaces for taking care of the constraint intopartition. So we would then have things likepartition_masks,n_theoretical_partitionsetcn_max_partitionswhich controls the max number of subspaces considered in any casereplace=True) or shuffle the order (shuffle=True). The latter is used when subsamapling the spaces because withshuffle=Trueyou can just instantiate the fistnobjects in the iterator to achieve random subsampling. This has some complications, especially for the overall searchspace class which needs to have a parameter to stop the process when replicated samples are drawn too often (likely no other feasible subspace combinations).botorch.pyin recommenders has become very large as a result of this development. So I split it into submodules_FixedNumericalContinuousParameterhad a bugged property which was namedis_numericbut it should beis_numericalContinuousCardinalityConstraintcan now also be applied in hybrid spaces, which fixesContinuousCardinalityConstraintnot considered in hybrid spaces #567Discuss:
n_theoretical_subspacesjust computes the upper limit of subspaces in discrete and hybrid cases. Because subspaces that do not have enough candidates are internally skipped the actual number ofn_feasible_subspacesmight be smaller. However that cannot be easily computed and would require instantiating all masks corresponding to the subspaces. The dispatcher logic is currently based onn_theoretical_subspacesbut to be 100% correct it should ben_feasible_subspaces- this is not implemented due to the mentioned difficulty. I think usingn_theoretical_subspacesworks as a proxy and will not be very different fromn_feasible_subspacesfor nearly all practical problems